package com.mobilesorcery.sdk.internal;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import com.mobilesorcery.sdk.core.IPropertyOwner;
import com.mobilesorcery.sdk.core.IProvider;
import com.mobilesorcery.sdk.core.ISecurePropertyOwner;
import com.mobilesorcery.sdk.core.PropertyUtil;
import com.mobilesorcery.sdk.core.SecurePropertyException;
import com.mobilesorcery.sdk.core.Util;
/**
* A utility class for encrypting/decrypting values. Encryption only occurs at
* save time and decryption only at load time, and the decrypted value is always
* stored in memory in cleartext.
*
* @author Mattias Bybro
*
*/
public class SecureProperties implements ISecurePropertyOwner {
private final static String NULL_VALUE = "__NULL__";
public final static String CIPHER = "PBEWithMD5AndDES"; //$NON-NLS-1$
public final static String KEY_FACTORY = "PBEWithMD5AndDES"; //$NON-NLS-1$
/**
* A default suffix for properties to indicate them to be secure properties;
* secure properties are usually stored in the local project file.
*/
public static final String DEFAULT_SECURE_PROPERTY_SUFFIX = ".secure";
private final IPropertyOwner delegate;
private final HashMap<String, String> cache = new HashMap<String, String>();
private IProvider<PBEKeySpec, String> passwordProvider;
private PBEKeySpec password;
private boolean passwordResolved = false;
private final String suffix;
/**
* Creates a new secure properties container.
* @param delegate
* @param passwordProvider A container for the encryption key; if
* this container returns {@code null}, no encryption will be
* performed
* @param suffix A suffix to add to property keys to indicate
* a secure property; if null orempty {@link #DEFAULT_SECURE_PROPERTY_SUFFIX}
* will be used.
* NOTE: We may want to refactor this.
*/
public SecureProperties(IPropertyOwner delegate,
IProvider<PBEKeySpec, String> passwordProvider,
String suffix) {
this.delegate = delegate;
this.passwordProvider = passwordProvider;
this.suffix = Util.isEmpty(suffix) ? DEFAULT_SECURE_PROPERTY_SUFFIX : suffix;
}
private Cipher createCipher(int mode, byte[] salt) throws GeneralSecurityException {
SecretKeyFactory keyFactory;
keyFactory = SecretKeyFactory.getInstance(KEY_FACTORY);
SecretKey key = keyFactory.generateSecret(password);
PBEParameterSpec entropy = new PBEParameterSpec(salt, 8);
Cipher cipher = Cipher.getInstance(CIPHER);
cipher.init(mode, key, entropy);
return cipher;
}
public static String generateRandomKey() throws GeneralSecurityException {
return generateRandomKey(512);
}
public static String generateRandomKey(int bits) throws GeneralSecurityException {
SecureRandom rnd = new SecureRandom();
byte[] result = new byte[bits / 8];
rnd.nextBytes(result);
return Util.toBase16(result);
}
private static byte[] generateSalt() {
byte[] salt = new byte[8];
SecureRandom random = new SecureRandom();
random.nextBytes(salt);
return salt;
}
public String encrypt(String value) throws GeneralSecurityException {
resolvePassword();
if (Util.isEmpty(value) || password == null) {
return value;
}
byte[] salt = generateSalt();
Cipher c = createCipher(Cipher.ENCRYPT_MODE, salt);
byte[] result = c.doFinal(value.getBytes(Charset.forName("UTF8")));
return PropertyUtil.fromStrings(new String[] { Util.toBase16(salt), Util.toBase16(result) });
}
public String decrypt(String value) throws GeneralSecurityException {
resolvePassword();
if (Util.isEmpty(value) || password == null) {
return value;
}
String[] saltAndPepper = PropertyUtil.toStrings(value);
if (saltAndPepper.length != 2) {
throw new GeneralSecurityException("Invalid encrypted data (salt is missing)");
}
byte[] salt = Util.fromBase16(saltAndPepper[0]);
byte[] toDecrypt = Util.fromBase16(saltAndPepper[1]);
Cipher c = createCipher(Cipher.DECRYPT_MODE, salt);
byte[] result = c.doFinal(toDecrypt);
return new String(result, Charset.forName("UTF8"));
}
@Override
public boolean setSecureProperty(String key, String value)
throws SecurePropertyException {
try {
String encrypted = encrypt(value);
cache.put(key, value == null ? NULL_VALUE : value);
return delegate.setProperty(key + suffix, encrypted);
} catch (GeneralSecurityException e) {
throw new SecurePropertyException("Unable to encrypt", e);
}
}
@Override
public String getSecureProperty(String key) throws SecurePropertyException {
String cached = cache.get(key);
if (cached != null) {
return cached == NULL_VALUE ? null : cached;
}
try {
String encrypted = delegate.getProperty(key + suffix);
String decrypted = encrypted == null ? NULL_VALUE : decrypt(encrypted);
cache.put(key, decrypted);
return decrypted;
} catch (GeneralSecurityException e) {
throw new SecurePropertyException("Unable to decrypt", e);
}
}
@Override
public void resetMasterPassword(IProvider<PBEKeySpec, String> newPasswordProvider) throws GeneralSecurityException, SecurePropertyException {
// Make sure we have the old password!
resolvePassword();
Map<String, String> delegateProperties = delegate.getProperties();
Map<String, String> decryptedProperties = new HashMap<String, String>();
for (String key : delegateProperties.keySet()) {
// Bah, overly complicated with all these suffices, will do for now.
String secureKey = getSecurePropertyKey(key);
if (secureKey != null) {
String decrypted = decryptOrClear(delegateProperties.get(key));
decryptedProperties.put(secureKey, decrypted);
}
}
// Ok, no exception!
this.passwordProvider = newPasswordProvider;
this.passwordResolved = false;
resolvePassword();
this.cache.clear();
for (String key : decryptedProperties.keySet()) {
setSecureProperty(key, decryptedProperties.get(key));
}
}
private String decryptOrClear(String value) {
try {
return decrypt(value);
} catch (GeneralSecurityException e) {
return null;
}
}
private void resolvePassword() throws GeneralSecurityException {
if (!passwordResolved) {
// MUST be lazily evaluated, see bug report MOSYNCTWOSIX-115.
password = passwordProvider.get(null);
passwordResolved = true;
}
}
private String getSecurePropertyKey(String key) {
if (key.endsWith(suffix)) {
return key.substring(0, key.length() - suffix.length());
}
return null;
}
}